/**
 * Copyright (C) 2010 Cloudfarming <info@cloudfarming.nl>
 *
 * Licensed under the Eclipse Public License - v 1.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.eclipse.org/legal/epl-v10.html
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package nl.cloudfarming.client.geoviewer.jxmap.map;

import nl.cloudfarming.client.geoviewer.jxmap.layerlist.LayerListPanel;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.Point;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.image.BufferedImage;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.HashSet;
import java.util.Set;
import javax.swing.JComponent;
import javax.swing.JLayeredPane;
import net.miginfocom.swing.MigLayout;
import nl.cloudfarming.client.geoviewer.LayerInfo;
import org.jdesktop.swingx.JXMapViewer;
import org.jdesktop.swingx.JXPanel;
import org.openide.explorer.ExplorerManager;

/**
 *
 * @author Timon Veenstra
 */
public class RootMapPanel extends JXPanel implements MouseListener, MouseMotionListener, MouseWheelListener {

    // swing container holding all layerPanels and the map panel
    private final JLayeredPane layerPanelContainer;
    // layers
    private static final Integer LAYER_MAP = Integer.valueOf(1);
    private static final Integer LAYER_IMAGE = Integer.valueOf(100);
    private static final Integer LAYER_PASSIVE = Integer.valueOf(200);
    private static final Integer LAYER_ACTIVE = Integer.valueOf(299);
    private static final Integer LAYER_DRAWING = Integer.valueOf(300);
    private static final Integer LAYER_LAYERLIST = Integer.valueOf(301);
    // dragging variables
    private Point startPressedPoint;
    private int mouseMovementX;
    private int mouseMovementY;
    private int panOffsetX;
    private int panOffsetY;
    private boolean mouseDragging;
    private boolean drawOldStateImage;
    // image to be drawn while dragging so layers will drag in sync
    private BufferedImage layeredPaneDragImage;
    private JXMapPanel mapPanel;
    private DrawingLayerPanel drawingLayerPanel;
    private final JXPanel layerListPanel;
    //
    // map with layerpanels for components
    //
    private final Set<LayerPanel> layerPanels = new HashSet<LayerPanel>();
    private ObjectLayerPanel activePanel;

//    private Map<Component, LayerInfo> layerInfoMap = new HashMap<Component, LayerInfo>();
    public RootMapPanel(ExplorerManager manager, LayerListPanel layerList) {
        setLayout(new BorderLayout());
        //
        // the layered pane which will contain the layers of the map
        //
        layerPanelContainer = new JLayeredPane();
        layerPanelContainer.setLayout(new FillLayout());
        add(layerPanelContainer, BorderLayout.CENTER);
        //
        // the map layer
        //
        mapPanel = new JXMapPanel(manager);
        layerPanelContainer.add(mapPanel, LAYER_MAP);
        //
        // the drawing layer
        //
        drawingLayerPanel = new DrawingLayerPanel();
        layerPanelContainer.add(drawingLayerPanel, LAYER_DRAWING);
        //
        // add the layerlist panel
        //
        layerListPanel = new JXPanel();
        layerListPanel.setOpaque(false);
        layerListPanel.setLayout(new MigLayout());
        layerListPanel.add(layerList);
        layerPanelContainer.add(layerListPanel, LAYER_LAYERLIST);
        //
        // mouse event listerner for drawing panel, top panel during dragging
        //
        drawingLayerPanel.addMouseListener(this);
        drawingLayerPanel.addMouseMotionListener(this);
        drawingLayerPanel.addMouseWheelListener(this);
        //
        // catch mouse events on the root map panel
        //
        addMouseListener(this);
        addMouseMotionListener(this);
        addMouseWheelListener(this);
    }

    @Override
    public void paint(Graphics grphcs) {
        super.paint(grphcs);

        if (this.drawOldStateImage) {
            // if panning, draw still image of old map panel state
            this.drawingLayerPanel.drawImage(this.layeredPaneDragImage, this.mouseMovementX + this.panOffsetX, this.mouseMovementY
                    + this.panOffsetY);
        }
    }

    private void dispatchToMap(MouseEvent e) {
        mapPanel.dispatchEvent(e);
    }

    @Override
    public void mouseClicked(MouseEvent me) {
        dispatchToMap(me);
    }

    @Override
    public void mousePressed(MouseEvent me) {
        this.startPressedPoint = me.getPoint();
        dispatchToMap(me);
    }

    @Override
    public void mouseReleased(MouseEvent e) {
        final Point point = e.getPoint();
        if (this.startPressedPoint != null) {
            this.mouseMovementX = (int) (point.getX() - this.startPressedPoint.getX());
            this.mouseMovementY = (int) (point.getY() - this.startPressedPoint.getY());
        }
        handleMouseReleasedPanMode();
        clearMouseMovement();

        this.drawOldStateImage = false;
        //
        // show  layers
        //
        for (Component c : this.layerPanelContainer.getComponentsInLayer(LAYER_ACTIVE)) {
            if (c.equals(this.activePanel) && this.activePanel.isVisible()) {
                c.setVisible(true);
            }
        }

        this.drawingLayerPanel.hideDrawingPanel();

        repaint();
        dispatchToMap(e);
    }

    @Override
    public void mouseEntered(MouseEvent me) {
        dispatchToMap(me);
    }

    @Override
    public void mouseExited(MouseEvent me) {
        dispatchToMap(me);
    }

    @Override
    public void mouseDragged(MouseEvent e) {
        final Point point = e.getPoint();
        if (this.startPressedPoint != null) {
            this.mouseMovementX = (int) (point.getX() - this.startPressedPoint.getX());
            this.mouseMovementY = (int) (point.getY() - this.startPressedPoint.getY());
        }
        handleMouseDraggingPanMode();

        repaint();
        dispatchToMap(e);
    }

    @Override
    public void mouseMoved(MouseEvent me) {
        dispatchToMap(me);
    }

    private void handleMouseDraggingPanMode() {
        if (!this.mouseDragging && !this.drawOldStateImage) {
            this.panOffsetX = 0;
            this.panOffsetY = 0;

            createPanImage();
        }
        this.mouseDragging = true;
    }

    private void createPanImage() {
        // create a still image of the current state of this map panel. This
        // image is following the mouse and thus
        // providing a visual clue that panning occurs
        // hide the underlying map panel when making the snapshot 
        mapPanel.setVisible(false);
        layerListPanel.setVisible(false);
        this.layeredPaneDragImage = createImage(layerPanelContainer, BufferedImage.TYPE_INT_ARGB_PRE);
        mapPanel.setVisible(true);
        layerListPanel.setVisible(true);

        this.drawingLayerPanel.paint(this.layeredPaneDragImage.getGraphics());
        //
        // hide object layers
        //
        for (Component c : this.layerPanelContainer.getComponentsInLayer(LAYER_ACTIVE)) {
            c.setVisible(false);
        }

        this.drawingLayerPanel.setVisible(true);

        // a separate flag indicating whether or not to draw the drag image is
        // needed.
        // mouseDragging is not sufficient because it is reset before the new
        // map is available.
        this.drawOldStateImage = true;
    }

    public static BufferedImage createImage(JComponent component, int imageType) {
        Dimension componentSize = component.getPreferredSize();
        component.setDoubleBuffered(false);
        component.setSize(componentSize);
        component.validate();

        BufferedImage img = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration().createCompatibleImage(component.getSize().width,
                component.getSize().height, imageType);
        Graphics2D grap = img.createGraphics();
        component.print(grap);
        grap.dispose();
        return img;
    }

    private void handleMouseReleasedPanMode() {
        this.mouseDragging = false;

        if (this.mouseMovementX != 0 || this.mouseMovementY != 0) {
            this.panOffsetX += this.mouseMovementX;
            this.panOffsetY += this.mouseMovementY;
        }

        requestFocusInWindow();
    }

    private void clearMouseMovement() {
        this.mouseMovementX = 0;
        this.mouseMovementY = 0;
        this.startPressedPoint = null;
    }

    @Override
    public void mouseWheelMoved(MouseWheelEvent mwe) {
        dispatchToMap(mwe);
    }

    /**
     * returns the mapviewer
     * 
     * @return
     */
    JXMapViewer getMapViewer() {
        return this.mapPanel;

    }

    /**
     * sets the active layer
     * null to set no active layer
     * 
     * @param lp
     */
    public void setActiveLayer(ObjectLayerPanel lp) {
        //
        // remove new active layer from painters and add previous one
        //
        if (this.activePanel != null) {
            this.activePanel.setActive(false);
            this.layerPanelContainer.setPosition(this.activePanel, LAYER_PASSIVE);
            this.activePanel.repaint();
        }

        this.activePanel = lp;

        if (lp != null) {
            this.layerPanelContainer.setPosition(this.activePanel, LAYER_ACTIVE);
            this.activePanel.setActive(true);
            this.activePanel.repaint();
        }
    }
    /**
     * internal method to add a layerpanel.
     * Only call from the AWT thread.
     *
     * @param layerPanel
     */
    private void internalAddLayerPanel(final LayerPanel layerPanel) {
        //
        // add interactive layers to the interaction layer
        // and add non interactive layer to the object layer
        //
        layerPanels.add(layerPanel);
        layerPanelContainer.add(layerPanel, (layerPanel.isInterActive()) ? LAYER_PASSIVE : LAYER_IMAGE);

        layerPanel.addPropertyChangeListener(new PropertyChangeListener() {

            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                if (LayerInfo.PROPERTY_VISIBLE.equals(evt.getPropertyName())) {
                    layerPanel.setVisible((Boolean) evt.getNewValue());
                }
            }
        });
    }

    /**
     * add a layer panel
     *
     * @param layerPanel
     */
    void addLayerPanel(final LayerPanel layerPanel) {
        //
        // do actual adding in the AWT thread because the scene could be validating and cause
        // concurrent modification exception
        //
        if (EventQueue.isDispatchThread()) {
            internalAddLayerPanel(layerPanel);
        } else {
            EventQueue.invokeLater(new Runnable() {

                @Override
                public void run() {
                    internalAddLayerPanel(layerPanel);
                }
            });
        }
    }

    /**
     * remove a layer panel
     * 
     * @param layerPanel
     */
    void removeLayerPanel(final LayerPanel layerPanel) {
        //
        // do actual removing in the AWT thread because the scene could be validating and cause
        // concurrent modification exception
        //
        if (EventQueue.isDispatchThread()) {
            internalRemoveLayerPanel(layerPanel);
        } else {
            EventQueue.invokeLater(new Runnable() {

                @Override
                public void run() {
                    internalRemoveLayerPanel(layerPanel);
                }
            });
        }

    }

    /**
     * internal method to remove a layerpanel.
     * Only call from the AWT thread.
     * 
     * @param layerPanel
     */
    private void internalRemoveLayerPanel(final LayerPanel layerPanel) {
        if (this.layerPanels.contains(layerPanel)) {
            this.layerPanels.remove(layerPanel);
        }
        layerPanelContainer.remove(layerPanel);
        getMapViewer().repaint();
    }
}
